package com.appspot.bibtable.util;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.security.AccessControlException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.logging.Logger;

public final class ReflectionUtility
{
	private static final Logger logger = Logger.getLogger(ReflectionUtility.class.getName());
	private static final String classExtension = ".class";
	private static final String packageSeparator = ".";

	private ReflectionUtility()
	{
		// Cannot be instanciated.
	}
	
	public static Collection<Class<?>> findClasses(Package p, ClassFilter filter)
	{
		return findClasses(p.getName(), filter);
	}

	public static Collection<Class<?>> findClasses(String packageName, ClassFilter filter)
	{
		if (filter == null)
		{
			filter = new ClassFilter()
			{
				@Override
				public boolean accept(Class<?> c)
				{
					return true;
				}
			};
		}
		
		Collection<Class<?>> classes = new ArrayList<Class<?>>();

		String classPaths = System.getProperty("java.class.path");
		final String pathSeparator = System.getProperty("path.separator");
		for (String path : classPaths.split(pathSeparator))
		{
			File file = new File(path);
			try
			{
				if (file.isDirectory())
				{
					readDirectory(packageName, path, classes, filter);
				}
				else if (file.isFile())
				{
					readArchive(packageName, path, classes, filter);
				}
			}
			catch (AccessControlException ex)
			{
				logger.warning(path + " is not accessible.");
			}
		}
		return classes;
	}

	private static void readDirectory(String packageName, String path, Collection<Class<?>> classes, ClassFilter filter)
	{
		String directoryPath = path + File.separator + packageName.replace(packageSeparator, File.separator);

		File directory = new File(directoryPath);
		File[] files = directory.listFiles(new FilenameFilter()
		{
			@Override
			public boolean accept(File dir, String name)
			{
				return name.toLowerCase().endsWith(classExtension);
			}
		});

		final int extensionLength = classExtension.length();
		for (File file : files)
		{
			String filePath = file.getPath().replace(path + File.separator, "");
			String className = filePathToPackageName(filePath);
			className = className.substring(0, className.length() - extensionLength);
			try
			{
				Class<?> c = Class.forName(className);
				if (filter.accept(c))
				{
					classes.add(c);
				}
			}
			catch (ClassNotFoundException ex)
			{
			}
		}
	}

	private static void readArchive(String packageName, String path, Collection<Class<?>> classes, ClassFilter filter)
	{
		JarInputStream stream = null;
		try
		{
			stream = new JarInputStream(new BufferedInputStream(new FileInputStream(path)));

			final int extensionLength = classExtension.length();
			String directory = packageNameToFilePath(packageName);
			JarEntry entry;
			while ((entry = stream.getNextJarEntry()) != null)
			{
				String name = entry.getName();
				if (name.indexOf(directory) == 0)
				{
					String className = filePathToPackageName(name);
					className = className.substring(0, className.length() - extensionLength);
					try
					{
						Class<?> c = Class.forName(className);
						if (filter.accept(c))
						{
							classes.add(c);
						}
					}
					catch (ClassNotFoundException ex)
					{
					}
				}
			}
		}
		catch (IOException ex)
		{
			logger.warning("Exception was raised when reading " + path + ".");
		}
		finally
		{
			IOUtility.closeSilently(stream);
		}
	}

	public static String packageNameToFilePath(String packageName)
	{
		return packageName.replace(packageSeparator, File.separator);
	}
	
	public static String filePathToPackageName(String filePath)
	{
		return filePath.replace(File.separator, packageSeparator);
	}
	
	public static boolean isSubclassOf(Class<?> c, Class<?> candidateSuperclass)
	{
		if (c == null)
		{
			throw new IllegalArgumentException();
		}
		if (candidateSuperclass == null)
		{
			return false;
		}

	   Class<?> superclass = c;

      while (superclass != null)
      {
         if (candidateSuperclass.equals(superclass))
         {
            return true;
         }
         superclass = superclass.getSuperclass();
      }
      return false;
	}

	public static Field[] getAllFields(Class<?> c)
	{
		return getAllFields(c, true);
	}
	
	public static Field[] getAllFields(Class<?> c, boolean subclass)
	{
		if (c == null)
		{
			throw new IllegalArgumentException();
		}

		List<Field> fields = new ArrayList<Field>();
		do
		{
			Collection<Field> declaredFields = Arrays.asList(c.getDeclaredFields());
			fields.addAll(0, declaredFields);
		} while (subclass && (c = c.getSuperclass()) != null);
		return fields.toArray(new Field[fields.size()]);
	}
	
	public static String getFullName(Member member)
	{
		return member.getDeclaringClass().getName() + "#" + member.getName();
	}
}
